/*
 * Copyright (c) 2021-2024 NVIDIA Corporation
 *
 * Licensed under the Apache License Version 2.0 with LLVM Exceptions
 * (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 *   https://llvm.org/LICENSE.txt
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
#pragma once

#include "__detail/__config.hpp"
#include "__detail/__meta.hpp"
#include "__detail/__tag_invoke.hpp"
#include "concepts.hpp"

#include <cstddef>
#include <functional>
#include <tuple>
#include <type_traits>

namespace stdexec
{
template <class _Fun0, class _Fun1>
struct __composed
{
    STDEXEC_ATTRIBUTE((no_unique_address))
    _Fun0 __t0_;
    STDEXEC_ATTRIBUTE((no_unique_address))
    _Fun1 __t1_;

    template <class... _Ts>
        requires __callable<_Fun1, _Ts...> &&
                 __callable<_Fun0, __call_result_t<_Fun1, _Ts...>>
    STDEXEC_ATTRIBUTE((always_inline))
    __call_result_t<_Fun0, __call_result_t<_Fun1, _Ts...>>
        operator()(_Ts&&... __ts) &&
    {
        return static_cast<_Fun0&&>(__t0_)(
            static_cast<_Fun1&&>(__t1_)(static_cast<_Ts&&>(__ts)...));
    }

    template <class... _Ts>
        requires __callable<const _Fun1&, _Ts...> &&
                 __callable<const _Fun0&, __call_result_t<const _Fun1&, _Ts...>>
    STDEXEC_ATTRIBUTE((always_inline))
    __call_result_t<_Fun0, __call_result_t<_Fun1, _Ts...>>
        operator()(_Ts&&... __ts) const&
    {
        return __t0_(__t1_(static_cast<_Ts&&>(__ts)...));
    }
};

inline constexpr struct __compose_t
{
    template <class _Fun0, class _Fun1>
    STDEXEC_ATTRIBUTE((always_inline))
    __composed<_Fun0, _Fun1> operator()(_Fun0 __fun0, _Fun1 __fun1) const
    {
        return {static_cast<_Fun0&&>(__fun0), static_cast<_Fun1&&>(__fun1)};
    }
} __compose{};

namespace __invoke_
{
template <class>
inline constexpr bool __is_refwrap = false;
template <class _Up>
inline constexpr bool __is_refwrap<std::reference_wrapper<_Up>> = true;

struct __funobj
{
    template <class _Fun, class... _Args>
    STDEXEC_ATTRIBUTE((always_inline))
    constexpr auto operator()(_Fun&& __fun, _Args&&... __args) const noexcept(
        noexcept((static_cast<_Fun&&>(__fun))(static_cast<_Args&&>(__args)...)))
        -> decltype((static_cast<_Fun&&>(__fun))(
            static_cast<_Args&&>(__args)...))
    {
        return static_cast<_Fun&&>(__fun)(static_cast<_Args&&>(__args)...);
    }
};

struct __memfn
{
    template <class _Memptr, class _Ty, class... _Args>
    STDEXEC_ATTRIBUTE((always_inline))
    constexpr auto operator()(_Memptr __mem_ptr, _Ty&& __ty,
                              _Args&&... __args) const
        noexcept(noexcept(((static_cast<_Ty&&>(__ty)).*
                           __mem_ptr)(static_cast<_Args&&>(__args)...)))
            -> decltype(((static_cast<_Ty&&>(__ty)).*
                         __mem_ptr)(static_cast<_Args&&>(__args)...))
    {
        return ((static_cast<_Ty&&>(__ty)).*__mem_ptr)(
            static_cast<_Args&&>(__args)...);
    }
};

struct __memfn_refwrap
{
    template <class _Memptr, class _Ty, class... _Args>
    STDEXEC_ATTRIBUTE((always_inline))
    constexpr auto operator()(_Memptr __mem_ptr, _Ty __ty,
                              _Args&&... __args) const
        noexcept(noexcept((__ty.get().*
                           __mem_ptr)(static_cast<_Args&&>(__args)...)))
            -> decltype((__ty.get().*
                         __mem_ptr)(static_cast<_Args&&>(__args)...))
    {
        return (__ty.get().*__mem_ptr)(static_cast<_Args&&>(__args)...);
    }
};

struct __memfn_smartptr
{
    template <class _Memptr, class _Ty, class... _Args>
    STDEXEC_ATTRIBUTE((always_inline))
    constexpr auto operator()(_Memptr __mem_ptr, _Ty&& __ty,
                              _Args&&... __args) const
        noexcept(noexcept(((*static_cast<_Ty&&>(__ty)).*
                           __mem_ptr)(static_cast<_Args&&>(__args)...)))
            -> decltype(((*static_cast<_Ty&&>(__ty)).*
                         __mem_ptr)(static_cast<_Args&&>(__args)...))
    {
        return ((*static_cast<_Ty&&>(__ty)).*__mem_ptr)(
            static_cast<_Args&&>(__args)...);
    }
};

struct __memobj
{
    template <class _Mbr, class _Class, class _Ty>
    STDEXEC_ATTRIBUTE((always_inline))
    constexpr auto operator()(_Mbr _Class::* __mem_ptr,
                              _Ty&& __ty) const noexcept
        -> decltype(((static_cast<_Ty&&>(__ty)).*__mem_ptr))
    {
        return ((static_cast<_Ty&&>(__ty)).*__mem_ptr);
    }
};

struct __memobj_refwrap
{
    template <class _Mbr, class _Class, class _Ty>
    STDEXEC_ATTRIBUTE((always_inline))
    constexpr auto operator()(_Mbr _Class::* __mem_ptr, _Ty __ty) const noexcept
        -> decltype((__ty.get().*__mem_ptr))
    {
        return (__ty.get().*__mem_ptr);
    }
};

struct __memobj_smartptr
{
    template <class _Mbr, class _Class, class _Ty>
    STDEXEC_ATTRIBUTE((always_inline))
    constexpr auto operator()(_Mbr _Class::* __mem_ptr,
                              _Ty&& __ty) const noexcept
        -> decltype(((*static_cast<_Ty&&>(__ty)).*__mem_ptr))
    {
        return ((*static_cast<_Ty&&>(__ty)).*__mem_ptr);
    }
};

auto __invoke_selector(__ignore, __ignore) noexcept -> __funobj;

template <class _Mbr, class _Class, class _Ty>
auto __invoke_selector(_Mbr _Class::*, const _Ty&) noexcept
{
    if constexpr (STDEXEC_IS_CONST(_Mbr) || STDEXEC_IS_CONST(const _Mbr))
    {
        // member function ptr case
        if constexpr (STDEXEC_IS_BASE_OF(_Class, _Ty))
        {
            return __memobj{};
        }
        else if constexpr (__is_refwrap<_Ty>)
        {
            return __memobj_refwrap{};
        }
        else
        {
            return __memobj_smartptr{};
        }
    }
    else
    {
        // member object ptr case
        if constexpr (STDEXEC_IS_BASE_OF(_Class, _Ty))
        {
            return __memfn{};
        }
        else if constexpr (__is_refwrap<_Ty>)
        {
            return __memfn_refwrap{};
        }
        else
        {
            return __memfn_smartptr{};
        }
    }
}

struct __invoke_t
{
    template <class _Fun>
    STDEXEC_ATTRIBUTE((always_inline))
    constexpr auto operator()(_Fun&& __fun) const
        noexcept(noexcept((static_cast<_Fun&&>(__fun))()))
            -> decltype((static_cast<_Fun&&>(__fun))())
    {
        return static_cast<_Fun&&>(__fun)();
    }

    template <class _Fun, class _Ty, class... _Args>
    STDEXEC_ATTRIBUTE((always_inline))
    constexpr auto operator()(_Fun&& __fun, _Ty&& __ty, _Args&&... __args) const
        noexcept(noexcept(__invoke_selector(__fun, __ty)(
            static_cast<_Fun&&>(__fun), static_cast<_Ty&&>(__ty),
            static_cast<_Args&&>(__args)...)))
            -> decltype(__invoke_selector(__fun, __ty)(
                static_cast<_Fun&&>(__fun), static_cast<_Ty&&>(__ty),
                static_cast<_Args&&>(__args)...))
    {
        return decltype(__invoke_selector(__fun, __ty))()(
            static_cast<_Fun&&>(__fun), static_cast<_Ty&&>(__ty),
            static_cast<_Args&&>(__args)...);
    }
};
} // namespace __invoke_

inline constexpr __invoke_::__invoke_t __invoke{};

template <class _Fun, class... _As>
concept __invocable = //
    requires(_Fun&& __f, _As&&... __as) {
        __invoke(static_cast<_Fun&&>(__f), static_cast<_As&&>(__as)...);
    };

template <class _Fun, class... _As>
concept __nothrow_invocable =    //
    __invocable<_Fun, _As...> && //
    requires(_Fun&& __f, _As&&... __as) {
        {
            __invoke(static_cast<_Fun&&>(__f), static_cast<_As&&>(__as)...)
        } noexcept;
    };

template <class _Fun, class... _As>
using __invoke_result_t = //
    decltype(__invoke(__declval<_Fun>(), __declval<_As>()...));

namespace __apply_
{
using std::get;

template <std::size_t... _Is, class _Fn, class _Tup>
STDEXEC_ATTRIBUTE((always_inline))
constexpr auto __impl(__indices<_Is...>, _Fn&& __fn, _Tup&& __tup) //
    noexcept(noexcept(__invoke(static_cast<_Fn&&>(__fn),
                               get<_Is>(static_cast<_Tup&&>(__tup))...)))
        -> decltype(__invoke(static_cast<_Fn&&>(__fn),
                             get<_Is>(static_cast<_Tup&&>(__tup))...))
{
    return __invoke(static_cast<_Fn&&>(__fn),
                    get<_Is>(static_cast<_Tup&&>(__tup))...);
}

template <class _Tup>
using __tuple_indices =
    __make_indices<std::tuple_size<std::remove_cvref_t<_Tup>>::value>;

template <class _Fn, class _Tup>
using __result_t = decltype(__apply_::__impl(
    __tuple_indices<_Tup>(), __declval<_Fn>(), __declval<_Tup>()));
} // namespace __apply_

template <class _Fn, class _Tup>
concept __applicable = __mvalid<__apply_::__result_t, _Fn, _Tup>;

template <class _Fn, class _Tup>
concept __nothrow_applicable =
    __applicable<_Fn, _Tup> //
    &&                      //
    noexcept(__apply_::__impl(__apply_::__tuple_indices<_Tup>(),
                              __declval<_Fn>(), __declval<_Tup>()));

template <class _Fn, class _Tup>
    requires __applicable<_Fn, _Tup>
using __apply_result_t = __apply_::__result_t<_Fn, _Tup>;

struct __apply_t
{
    template <class _Fn, class _Tup>
        requires __applicable<_Fn, _Tup>
    STDEXEC_ATTRIBUTE((always_inline))
    constexpr auto operator()(_Fn&& __fn, _Tup&& __tup) const
        noexcept(__nothrow_applicable<_Fn, _Tup>) -> __apply_result_t<_Fn, _Tup>
    {
        return __apply_::__impl(__apply_::__tuple_indices<_Tup>(),
                                static_cast<_Fn&&>(__fn),
                                static_cast<_Tup&&>(__tup));
    }
};

inline constexpr __apply_t __apply{};

template <class _Tag, class _Ty>
struct __field
{
    STDEXEC_ATTRIBUTE((always_inline))
    _Ty operator()(_Tag) const noexcept(__nothrow_decay_copyable<const _Ty&>)
    {
        return __t_;
    }

    _Ty __t_;
};

template <class _Tag>
struct __mkfield_
{
    template <class _Ty>
    STDEXEC_ATTRIBUTE((always_inline))
    __field<_Tag, __decay_t<_Ty>> operator()(_Ty&& __ty) const
        noexcept(__nothrow_decay_copyable<_Ty>)
    {
        return {static_cast<_Ty&&>(__ty)};
    }
};

template <class _Tag>
inline constexpr __mkfield_<_Tag> __mkfield{};
} // namespace stdexec
